package com.lcf.app;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacpp.Loader;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import ws.schild.jave.Encoder;
import ws.schild.jave.EncoderException;
import ws.schild.jave.MultimediaObject;
import ws.schild.jave.encode.AudioAttributes;
import ws.schild.jave.encode.EncodingAttributes;
import ws.schild.jave.encode.VideoAttributes;
import ws.schild.jave.info.MultimediaInfo;
import ws.schild.jave.progress.EncoderProgressListener;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 音视频工具类：
 * <br/>
 * 参考文档：https://blog.csdn.net/JineD/article/details/123057086
 */
public class FFmpegUtils {

    private static final String FFMPEG = Loader.load(org.bytedeco.ffmpeg.ffmpeg.class);

    private static final String FFPROBE = Loader.load(org.bytedeco.ffmpeg.ffprobe.class);

    /**
     * 剪切视频
     * <br/>
     * eg : FFmpegUtils.cutVideo(input, output, "00:00:10", 30);
     *
     * @param input    : 输入文件
     * @param output   : 输出文件
     * @param start    : 开始时间
     * @param duration : 持续时间
     */
    public static boolean cutVideo(String input, String output, String start, int duration) {
        String[] command = {
                FFMPEG,
                "-ss", // 开始时间
                start,
                "-t", // 持续时间
                String.valueOf(duration),
                "-i", // 指定输入文件
                input,
                "-vcodec", // 设定视频编解码器，未设定时则使用与输入流相同的编解码器
                "copy", // 直接复制原视频编码格式
                "-acodec", // 设定声音编解码器，未设定时则使用与输入流相同的编解码器
                "copy", // 直接复制原视频编码格式
                output, // 输出的文件路径
                "-y" // 不经过确认，输出时直接覆盖同名文件。
        };
        return executeCommand(command);
    }

    /**
     * 剪切视频
     * <br/>
     * eg : FFmpegUtils.cutVideo(input, output, "00:00:10", "00:00:15");
     *
     * @param input  : 输入文件
     * @param output : 输出文件
     * @param start  : 开始时间
     * @param end    : 结束时间
     */
    public static boolean cutVideo(String input, String output, String start, String end) {
        String[] command = {
                FFMPEG,
                "-ss", // 开始时间
                start,
                "-to", // 结束时间
                end,
                "-i", // 指定输入文件
                input,
                "-vcodec", // 设定视频编解码器，未设定时则使用与输入流相同的编解码器
                "copy", // 直接复制原视频编码格式
                "-acodec", // 设定声音编解码器，未设定时则使用与输入流相同的编解码器
                "copy", // 直接复制原视频编码格式
                output, // 输出的文件路径
                "-y" // 不经过确认，输出时直接覆盖同名文件。
        };
        return executeCommand(command);
    }

    /**
     * 剪切音频
     * <br/>
     * eg : FFmpegUtils.cutAudio(input, output, "00:00:10", 30);
     *
     * @param input    : 输入文件
     * @param output   : 输出文件
     * @param start    : 开始时间
     * @param duration : 持续时间
     */
    public static boolean cutAudio(String input, String output, String start, int duration) {
        String[] command = {
                FFMPEG,
                "-ss", // 开始时间
                start,
                "-t", // 持续时间
                String.valueOf(duration),
                "-i", // 指定输入文件
                input,
                "-acodec", // 设定声音编解码器，未设定时则使用与输入流相同的编解码器
                "copy", // 直接复制原视频编码格式
                output, // 输出的文件路径
                "-y" // 不经过确认，输出时直接覆盖同名文件。
        };
        return executeCommand(command);
    }

    /**
     * 剪切音频
     * <br/>
     * eg : FFmpegUtils.cutAudio(input, output, "00:00:10", "00:00:20");
     *
     * @param input  : 输入文件
     * @param output : 输出文件
     * @param start  : 开始时间
     * @param end    : 结束时间
     */
    public static boolean cutAudio(String input, String output, String start, String end) {
        String[] command = {
                FFMPEG,
                "-ss", // 开始时间
                start,
                "-to", // 结束时间
                end,
                "-i", // 指定输入文件
                input,
                "-acodec", // 设定声音编解码器，未设定时则使用与输入流相同的编解码器
                "copy", // 直接复制原视频编码格式
                output, // 输出的文件路径
                "-y" // 不经过确认，输出时直接覆盖同名文件。
        };
        return executeCommand(command);
    }

    /**
     * 剪切音视频
     * <br/>
     * FFmpegUtils.cut(input, output, "00:00:10", 30);
     *
     * @param input    : 输入文件
     * @param output   : 输出文件
     * @param start    : 开始时间
     * @param duration : 持续时间
     */
    public static boolean cut(String input, String output, String start, int duration) {
        String[] command = {
                FFMPEG,
                "-ss", // 开始时间
                start,
                "-t", // 持续时间
                String.valueOf(duration),
                "-i", // 指定输入文件
                input,
                "-c", // 直接复制，不经过重新编码
                "copy",
                output, // 输出的文件路径
                "-y" // 不经过确认，输出时直接覆盖同名文件。
        };
        return executeCommand(command);
    }

    /**
     * 剪切音视频
     * <br/>
     * FFmpegUtils.cut(input, output, "00:00:10", "00:00:20");
     *
     * @param input  : 输入文件
     * @param output : 输出文件
     * @param start  : 开始时间
     * @param end    : 结束时间
     */
    public static boolean cut(String input, String output, String start, String end) {
        String[] command = {
                FFMPEG,
                "-ss", // 开始时间
                start,
                "-to", // 结束时间
                end,
                "-i", // 指定输入文件
                input,
                "-c", // 直接复制，不经过重新编码
                "copy",
                output, // 输出的文件路径
                "-y" // 不经过确认，输出时直接覆盖同名文件。
        };
        return executeCommand(command);
    }

    /**
     * 删除指定范围的音视频，将剩余的合并
     * <br/>
     * FFmpegUtils.removeFrom(input, output, "00:00:10", "00:00:20");
     *
     * @param input  : 输入文件
     * @param output : 输出文件
     * @param start  : 开始时间
     * @param end    : 结束时间
     */
    public static boolean removeFrom(String input, String output, String start, String end) {
        String duration = LocalTime.ofSecondOfDay(duration(input)).format(DateTimeFormatter.ofPattern("HH:mm:ss"));
        if ("00:00:00".equals(start) && duration.equals(end)) { // 开始时间等于视频开始时间，并且结束时间等于视频时长。
            FileUtil.copy(input, output, true);
            return true;
        }
        if ("00:00:00".equals(start)) { // 开始时间等于视频开始时间
            return cut(input, output, end, duration);
        }
        if (duration.equals(end)) { // 结束时间等于视频时长
            return cut(input, output, "00:00:00", start);
        }
        // 先生成两个文件，再合并。
        List<String> inputs = new ArrayList<>();
        String tmpdir = System.getProperty("java.io.tmpdir") + "media/";
        FileUtil.mkdir(tmpdir);
        String output1 = tmpdir + UUID.randomUUID() + "." + StrUtil.subAfter(output, ".", true);
        String output2 = tmpdir + UUID.randomUUID() + "." + StrUtil.subAfter(output, ".", true);
        cut(input, output1, "00:00:00", start);// 剪切前边的片段
        cut(input, output2, end, duration); // 剪切后边的片段
        inputs.add(output1);
        inputs.add(output2);
        // 合并
        boolean merge = merge(inputs, output);
        FileUtil.del(tmpdir);
        return merge;
    }

    /**
     * 合并音视频
     *
     * @param inputs
     * @param output
     * @return
     */
    public static boolean merge(List<String> inputs, String output) {
        String tmpFile = System.getProperty("java.io.tmpdir") + UUID.randomUUID() + ".txt";
        String joinStr = inputs.stream().map(v -> String.format("file '%s'", v)).collect(Collectors.joining("\n"));
        File fileListTxt = FileUtil.writeString(joinStr, tmpFile, StandardCharsets.UTF_8);
        String[] command = {
                FFMPEG,
                "-f", // `-f concat`指定使用`concat`协议
                "concat",
                "-safe", // `-safe 0`指定不检查文件路径的安全性
                "0",
                "-i", // `-i fileListTxt.txt`指定输入文件列表
                fileListTxt.getPath(),
                "-c", // `-c copy`指定使用复制编解码器进行快速合并
                "copy",
                output, // 输出的文件路径
                "-y" // 不经过确认，输出时直接覆盖同名文件。
        };
        boolean b = executeCommand(command);
        FileUtil.del(fileListTxt); // 删除文件
        return b;
    }

    /**
     * 获取音视频时长
     *
     * @param input
     * @return
     */
    public static int duration(String input) {
        try (
                // 创建FrameGrabber
                FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(input)
        ) {
            grabber.start();
            return (int) (grabber.getLengthInTime() / 1000000); // 微妙转为秒
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 根据时间获第一帧图片
     * <br/>
     * FFmpegUtils.getVideoImg("input.mkv","output.jpg", 10);
     *
     * @param input
     * @param output
     * @param ss     : 时间（秒）
     * @return
     */
    public static boolean getVideoImg(String input, String output, int ss) {
        String[] command = {
                FFMPEG,
                "-i", // -i`表示输入文件
                input,
                "-ss", // `-ss`表示要获取的帧的时间位置，
                String.valueOf(ss),
                "-vframes", // `-vframes`表示要获取的帧数
                "1",
                output, // 输出的文件路径
                "-y" // 不经过确认，输出时直接覆盖同名文件。
        };
        return executeCommand(command);
    }


    /**
     * 转换视频格式: （没有转化视频的编码格式）
     * <br/>
     * FFmpegUtils.convert("input.mkv","output.mp4");
     *
     * @param input
     * @param output
     * @return
     */
    public static boolean convert(String input, String output) {
        String[] command = {
                FFMPEG,
                "-i", // 指定输入文件
                input,
                "-c:v", // 直接复制，不经过重新编码
                //"copy",
                "h264",
                output, // 输出的文件路径
                "-y" // 不经过确认，输出时直接覆盖同名文件。
        };
        return executeCommand(command);
    }

    public static void help() {
        String[] command = {FFMPEG, "-help"};
        executeCommand(command);
    }

    /**
     * 执行命令
     */
    public static boolean executeCommand(String[] command) {
        try {
            ProcessBuilder builder = new ProcessBuilder(command);
            String collect = String.join(" ", builder.command());
            System.out.println("************************************ COMMAND ************************************");
            System.out.println(collect);
            System.out.println("************************************ COMMAND ************************************");
            Process process = builder.inheritIO().start();
            return 0 == process.waitFor(); // 0表示正常终止
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    // 给视频添加logo
    // ffmpeg -i h:\work\video.mp4 -i h:\work\psu.png -filter_complex ‘overlay’ d:\work\video.mp4

    // --------------------------------- 下边的方法不要用了；效率太慢了。

    /**
     * 裁剪音频
     *
     * @param inputFile  : 输入文件
     * @param outPutFile : 输出文件
     * @param startMs    : 开始毫秒数
     * @param endMs      : 结束毫秒数
     * @param subtract   : true： 去掉该范围内的数据；false：获取范围内的数据
     * @throws Exception
     */
    public static boolean audioClip(String inputFile, String outPutFile, int startMs, int endMs, boolean subtract) throws Exception {
        FFmpegFrameGrabber grabber = null;
        FFmpegFrameRecorder recorder = null;
        try {
            // create grabber for input file
            grabber = new FFmpegFrameGrabber(inputFile);
            grabber.start();
            //Map<String, String> audioMetadata = grabber.getMetadata(); // 电子文件元数据
            // create recorder for output file
            recorder = new FFmpegFrameRecorder(outPutFile, grabber.getAudioChannels());
            recorder.setAudioCodec(grabber.getAudioCodec());
            recorder.setAudioBitrate(grabber.getAudioBitrate());
            recorder.setAudioChannels(grabber.getAudioChannels());
            //
            recorder.setSampleRate(grabber.getSampleRate());
            recorder.setFrameRate(grabber.getFrameRate());
            recorder.setFormat(grabber.getFormat());
            recorder.start();

            // read and write audio frames
            Frame frame;
            // 获取帧数据
            while ((frame = grabber.grabFrame()) != null) {
                if (frame.samples == null) {
                    continue;
                }
                // 获取当前帧所在毫秒
                double timestamp = frame.timestamp / 1000.0;
                if (subtract) {
                    if (timestamp <= startMs || timestamp >= endMs) {
                        recorder.record(frame); // 记录当前帧
                    }
                } else {
                    if (timestamp >= startMs && timestamp <= endMs) {
                        recorder.record(frame); // 记录当前帧
                    }
                    if (timestamp > endMs) {
                        break;
                    }
                }
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            if (recorder != null) {
                // 释放资源
                recorder.close();
            }
            if (grabber != null) {
                grabber.close();
            }
        }
    }

    /**
     * 裁剪视频
     *
     * @param inputFile  : 输入文件
     * @param outPutFile : 输出文件
     * @param startMs    : 开始毫秒数
     * @param endMs      : 结束毫秒数
     * @param subtract   : true： 去掉该范围内的数据；false：获取范围内的数据
     * @throws Exception
     */
    public static boolean videoClip(String inputFile, String outPutFile, int startMs, int endMs, boolean subtract) throws Exception {
        FFmpegFrameGrabber grabber = null;
        FFmpegFrameRecorder recorder = null;
        try {
            // 创建FrameGrabber和FrameRecorder对象
            grabber = new FFmpegFrameGrabber(inputFile);
            grabber.start();
            grabber.setTimestamp(1000L * startMs);// 视频截取的开始时间
            recorder = new FFmpegFrameRecorder(outPutFile, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
            recorder.setVideoCodec(grabber.getVideoCodec());
            recorder.setVideoCodecName(grabber.getVideoCodecName());
            recorder.setVideoBitrate(grabber.getVideoBitrate());
            //
            recorder.setFormat(grabber.getFormat());
            recorder.setFrameRate(grabber.getFrameRate());
            recorder.setSampleRate(grabber.getSampleRate());
            recorder.start();
            // 读取帧和写入视频帧
            Frame frame;
            while ((frame = grabber.grab()) != null) {
                double timestamp = frame.timestamp / 1000.0;
                if (timestamp >= startMs && timestamp <= endMs) {
                    recorder.record(frame);
                }
                if (timestamp > endMs) {
                    break;
                }
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        } finally {
            if (recorder != null) {
                recorder.close();
            }
            if (grabber != null) {
                grabber.close();
            }
        }
    }


    /**
     * 获取视频封面
     *
     * @param inputStream
     * @return
     */
    public static BufferedImage videoCoverImage(InputStream inputStream) {
        try (
                Java2DFrameConverter converter = new Java2DFrameConverter();
                // 创建FrameGrabber
                FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputStream)
        ) {
            grabber.start();
            int framesLength = grabber.getLengthInVideoFrames();
            for (int i = 0; i < framesLength; i++) {
                Frame frame = grabber.grab();
                if (frame != null) {
                    return converter.convert(frame);
                }
            }
            return null;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    public static Map<String, String> metadata(InputStream inputStream) {
        try (
                FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(inputStream)
        ) {
            grabber.start();
            return grabber.getMetadata();
        } catch (Exception e) {
            e.printStackTrace();
            return new HashMap<>();
        }
    }

    public static void command(String s) {
        String[] command = {FFMPEG, s};
        executeCommand(command);
    }

    public static String convertToMp4(File file) {

        FFmpegFrameGrabber frameGrabber = new FFmpegFrameGrabber(file);
        String fileName = null;
        Frame captured_frame = null;
        FFmpegFrameRecorder recorder = null;
        try {
            frameGrabber.start();
            fileName = file.getAbsolutePath() + "__.mp4";
            recorder = new FFmpegFrameRecorder(fileName, frameGrabber.getImageWidth(), frameGrabber.getImageHeight(), frameGrabber.getAudioChannels());
            recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
            recorder.setFormat("mp4");
            recorder.setFrameRate(frameGrabber.getFrameRate());
            recorder.setSampleRate(frameGrabber.getSampleRate());

            recorder.setAudioChannels(frameGrabber.getAudioChannels());
            recorder.setFrameRate(frameGrabber.getFrameRate());
            recorder.start();
            while ((captured_frame = frameGrabber.grabFrame()) != null) {
                try {
                    recorder.record(captured_frame);
                } catch (Exception e) {
                    System.out.println(e);
                }
            }
            recorder.stop();
            recorder.release();
            frameGrabber.stop();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return fileName;
    }

    public static void videoToMp4(String input, String output) throws EncoderException, MalformedURLException {
        VideoAttributes videoAttributes = new VideoAttributes();
        videoAttributes.setCodec("libx264");
        AudioAttributes audioAttributes = new AudioAttributes();
        audioAttributes.setCodec("aac");
        //
        EncodingAttributes encodingAttributes = new EncodingAttributes();
        encodingAttributes.setOutputFormat("mp4");
        encodingAttributes.setVideoAttributes(videoAttributes);
        encodingAttributes.setAudioAttributes(audioAttributes);
        // 编码
        encode(input, new File(output), encodingAttributes);
    }

    public static void audioToMp3(String input, String output) throws EncoderException, MalformedURLException {
        AudioAttributes audioAttributes = new AudioAttributes();
        audioAttributes.setCodec("libmp3lame");
        //
        EncodingAttributes encodingAttributes = new EncodingAttributes();
        encodingAttributes.setOutputFormat("mp3");
        encodingAttributes.setAudioAttributes(audioAttributes);
        // 编码
        encode(input, new File(output), encodingAttributes);
    }

    private static void encode(String input, File output, EncodingAttributes encodingAttributes) throws EncoderException, MalformedURLException {
        Encoder encoder = new Encoder();
        MultimediaObject multimediaObject;
        if (input.startsWith("http")) {
            multimediaObject = new MultimediaObject(new URL(input));
        } else {
            multimediaObject = new MultimediaObject(new File(input));
        }
        encoder.encode(
                multimediaObject
                , output, encodingAttributes, new EncoderProgressListener() {
                    @Override
                    public void sourceInfo(MultimediaInfo info) {
                        System.err.println("元数据: " + info);
                    }

                    @Override
                    public void progress(int permil) {
                        System.err.printf("进度: %s/1000 %n", permil);
                    }

                    @Override
                    public void message(String message) {
                        System.err.println("message: " + message);
                    }
                });
    }

    public static void main(String[] args) {
        String cmd = FFPROBE + " -v error -select_streams v:0 -show_entries stream=width,height -of default=noprint_wrappers=1:nokey=1 %s";
        cmd = String.format(cmd, "C:\\Users\\ljd\\Desktop\\音视频测试文件\\各种格式\\111.FLV");
        String[] arr = cmd.split(" ");
        executeCommand(arr);

        //convert("C:\\Users\\ljd\\Desktop\\音视频测试文件\\各种格式\\111.FLV", "C:\\Users\\ljd\\Desktop\\音视频测试文件\\各种格式\\1110.mp4");

        // ffmpeg -codecs | grep '^..V'   查看FFmpeg已安装支持的视频编码格式
        // ffmpeg -codecs | grep '^..A'   查看FFmpeg支持的音频编码格式
        String vvv = FFMPEG + " -codecs | grep '^..V'";
        executeCommand(vvv.split(" "));

    }

}
